"""Home Connect entity base class."""

from abc import abstractmethod
from collections.abc import Callable, Coroutine
import contextlib
from datetime import datetime
import logging
from typing import Any, Concatenate, cast

from aiohomeconnect.model import EventKey, OptionKey
from aiohomeconnect.model.error import (
    ActiveProgramNotSetError,
    HomeConnectError,
    TooManyRequestsError,
)

from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import API_DEFAULT_RETRY_AFTER, DOMAIN
from .coordinator import HomeConnectApplianceData, HomeConnectCoordinator
from .utils import get_dict_from_home_connect_error

_LOGGER = logging.getLogger(__name__)


class HomeConnectEntity(CoordinatorEntity[HomeConnectCoordinator]):
    """Generic Home Connect entity (base class)."""

    _attr_has_entity_name = True

    def __init__(
        self,
        coordinator: HomeConnectCoordinator,
        appliance: HomeConnectApplianceData,
        desc: EntityDescription,
        context_override: Any | None = None,
    ) -> None:
        """Initialize the entity."""
        context = (appliance.info.ha_id, EventKey(desc.key))
        if context_override is not None:
            context = context_override
        super().__init__(coordinator, context)
        self.appliance = appliance
        self.entity_description = desc
        self._attr_unique_id = f"{appliance.info.ha_id}-{desc.key}"
        self._attr_device_info = DeviceInfo(
            identifiers={(DOMAIN, appliance.info.ha_id)},
        )
        self.update_native_value()

    @abstractmethod
    def update_native_value(self) -> None:
        """Set the value of the entity."""

    @callback
    def _handle_coordinator_update(self) -> None:
        """Handle updated data from the coordinator."""
        self.update_native_value()
        available = self._attr_available = self.appliance.info.connected
        self.async_write_ha_state()
        state = STATE_UNAVAILABLE if not available else self.state
        _LOGGER.debug("Updated %s, new state: %s", self.entity_id, state)

    @property
    def bsh_key(self) -> str:
        """Return the BSH key."""
        return self.entity_description.key

    @property
    def available(self) -> bool:
        """Return True if entity is available.

        Do not use self.last_update_success for available state
        as event updates should take precedence over the coordinator
        refresh.
        """
        return self._attr_available


class HomeConnectOptionEntity(HomeConnectEntity):
    """Class for entities that represents program options."""

    @property
    def available(self) -> bool:
        """Return True if entity is available."""
        return super().available and self.bsh_key in self.appliance.options

    @property
    def option_value(self) -> str | int | float | bool | None:
        """Return the state of the entity."""
        if event := self.appliance.events.get(EventKey(self.bsh_key)):
            return event.value
        return None

    async def async_set_option(self, value: str | float | bool) -> None:
        """Set an option for the entity."""
        try:
            # We try to set the active program option first,
            # if it fails we try to set the selected program option
            with contextlib.suppress(ActiveProgramNotSetError):
                await self.coordinator.client.set_active_program_option(
                    self.appliance.info.ha_id,
                    option_key=self.bsh_key,
                    value=value,
                )
                _LOGGER.debug(
                    "Updated %s for the active program, new state: %s",
                    self.entity_id,
                    self.state,
                )
                return

            await self.coordinator.client.set_selected_program_option(
                self.appliance.info.ha_id,
                option_key=self.bsh_key,
                value=value,
            )
            _LOGGER.debug(
                "Updated %s for the selected program, new state: %s",
                self.entity_id,
                self.state,
            )
        except HomeConnectError as err:
            raise HomeAssistantError(
                translation_domain=DOMAIN,
                translation_key="set_option",
                translation_placeholders=get_dict_from_home_connect_error(err),
            ) from err

    @property
    def bsh_key(self) -> OptionKey:
        """Return the BSH key."""
        return cast(OptionKey, self.entity_description.key)


def constraint_fetcher[_EntityT: HomeConnectEntity, **_P](
    func: Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, Any]],
) -> Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, None]]:
    """Decorate the function to catch Home Connect too many requests error and retry later.

    If it needs to be called later, it will call async_write_ha_state function
    """

    async def handler_to_return(
        self: _EntityT, *args: _P.args, **kwargs: _P.kwargs
    ) -> None:
        async def handler(_datetime: datetime | None = None) -> None:
            try:
                await func(self, *args, **kwargs)
            except TooManyRequestsError as err:
                if (retry_after := err.retry_after) is None:
                    retry_after = API_DEFAULT_RETRY_AFTER
                async_call_later(self.hass, retry_after, handler)
            except HomeConnectError as err:
                _LOGGER.error(
                    "Error fetching constraints for %s: %s", self.entity_id, err
                )
            else:
                if _datetime is not None:
                    self.async_write_ha_state()

        await handler()

    return handler_to_return
